package io.xjar;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.stream.Collectors;

import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.maven.model.Build;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.xml.Xpp3Dom;

import io.xjar.boot.XBoot;
import io.xjar.filter.XAllEntryFilter;
import io.xjar.filter.XAnyEntryFilter;
import io.xjar.filter.XMixEntryFilter;
import io.xjar.jar.XJar;
import io.xjar.key.XKey;
import io.xjar.loadkit.Loaders;
import io.xjar.loadkit.Resource;
import io.xjar.zip.GZIPUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * XJar 构建插件
 * <p>
 * <p>
 * 2018/12/4 10:27
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Mojo(name = "build", defaultPhase = LifecyclePhase.PACKAGE)
public class XBuilder extends AbstractMojo {
    private static final String LINUX = "linux";
    private static final String WINDOWS = "win";
    private static final String MAC = "mac";
    private static List<String> arch = Arrays.asList("amd64", "arm64");

    private static final String TPL_ARCH = "#{archList}";
    private static final String TPL_TARGETDIR = "#{targetDir}";
    private static final String TPL_GOFILEPATH = "#{goFilePath}";


    /**
     * 当前Maven工程
     */
    @Parameter(defaultValue = "${project}", readonly = true, required = true)
    private MavenProject project;
    /**
     * 加密算法名称
     */
    @Parameter(property = "xjar.algorithm", required = true, defaultValue = "AES/CBC/PKCS5Padding")
    private String algorithm;
    /**
     * 加密密钥长度
     */
    @Parameter(property = "xjar.keySize", required = true, defaultValue = "128")
    private int keySize;
    /**
     * 加密向量长度
     */
    @Parameter(property = "xjar.ivSize", required = true, defaultValue = "128")
    private int ivSize;
    /**
     * 加密密码
     */
    @Parameter(property = "xjar.password", required = true)
    private String password;
    /**
     * 原本JAR所在文件夹
     */
    @Parameter(property = "xjar.sourceDir", required = true, defaultValue = "${project.build.directory}")
    private File sourceDir;
    /**
     * 原本JAR名称
     */
    @Parameter(property = "xjar.sourceJar", required = true, defaultValue = "${project.build.finalName}.jar")
    private String sourceJar;
    /**
     * 生成JAR所在文件夹
     */
    @Parameter(property = "xjar.targetDir", required = true, defaultValue = "${project.build.directory}")
    private File targetDir;
    /**
     * 生成JAR名称
     */
    @Parameter(property = "xjar.targetJar", required = true, defaultValue = "${project.build.finalName}-x.jar")
    private String targetJar;
    /**
     * 包含资源。
     * 使用Ant表达式，例如：
     * io/xjar/**
     * com/company/project/**
     * mapper/*Mapper.xml
     */
    @Parameter(property = "xjar.includes")
    private String[] includes;
    /**
     * 排除资源。
     * 使用Ant表达式，例如：
     * io/xjar/**
     * static/**
     * META-INF/resources/**
     */
    @Parameter(property = "xjar.excludes")
    private String[] excludes;
    /**
     * 加密完成后需要删除的资源
     * 支持Ant表达式，例如：
     * target/*.jar
     * ../module/target/*.jar
     */
    @Parameter(property = "xjar.deletes")
    private String[] deletes;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        Log log = getLog();
        String packaging = project.getPackaging();
        if (!"jar".equalsIgnoreCase(packaging)) {
            log.info("Skip for packaging: " + packaging);
            return;
        }
        try {
//            if (log.isDebugEnabled()) {
//                log.debug("Using algorithm: " + algorithm);
//                log.debug("Using key-size: " + keySize);
//                log.debug("Using iv-size: " + ivSize);
//                log.debug("Using password: " + password);
//            }

            XKey xkey = XKit.key(algorithm, keySize, ivSize, password);
            File src = new File(sourceDir, sourceJar);
            File dest = new File(targetDir, targetJar);
            File folder = dest.getParentFile();
            if (!folder.exists() && !folder.mkdirs() && !folder.exists()) {
                throw new IOException("could not make directory: " + folder);
            }
            log.info("Building xjar: " + dest + " for jar: " + src);

            XMixEntryFilter<JarArchiveEntry> filter;
            if (XArray.isEmpty(includes) && XArray.isEmpty(excludes)) {
                filter = null;
                log.info("Including all resources");
            } else {
                filter = XKit.all();
                if (!XArray.isEmpty(includes)) {
                    XAnyEntryFilter<JarArchiveEntry> including = XKit.any();
                    for (String include : includes) {
                        including.mix(new XIncludeAntEntryFilter(include));
                        log.info("Including " + include);
                    }
                    filter.mix(including);
                }
                if (!XArray.isEmpty(excludes)) {
                    XAllEntryFilter<JarArchiveEntry> excluding = XKit.all();
                    for (String exclude : excludes) {
                        excluding.mix(new XExcludeAntEntryFilter(exclude));
                        log.info("Excluding " + exclude);
                    }
                    filter.mix(excluding);
                }
            }

            Build build = project.getBuild();
            Map<String, Plugin> plugins = build.getPluginsAsMap();
            Plugin plugin = plugins.get("org.springframework.boot:spring-boot-maven-plugin");
            // 非Spring-Boot项目/模块
            if (plugin == null) {
                XJar.encrypt(src, dest, xkey, filter);
            }
            // Spring-Boot项目/模块
            else {
                Object configuration = plugin.getConfiguration();
                // 不允许开启 <executable>true<executable>
                if (configuration instanceof Xpp3Dom) {
                    Xpp3Dom dom = (Xpp3Dom) configuration;
                    {
                        Xpp3Dom child = dom.getChild("executable");
                        String executable = child != null ? child.getValue() : null;
                        if ("true".equalsIgnoreCase(executable)) {
                            String msg =
                                    "Unsupported to build an xjar for an <executable>true</executable> spring boot "
                                            + "JAR file, ";
                            msg +=
                                    "maybe you should upgrade xjar-maven-plugin dependency if it have been supported "
                                            + "in the later versions,";
                            msg +=
                                    "if not, delete <executable>true</executable> or set executable as false for the "
                                            + "configuration of spring-boot-maven-plugin.";
                            throw new MojoFailureException(msg);
                        }
                    }
                    {
                        Xpp3Dom child = dom.getChild("embeddedLaunchScript");
                        String embeddedLaunchScript = child != null ? child.getValue() : null;
                        if (embeddedLaunchScript != null) {
                            String msg =
                                    "Unsupported to build an xjar for an <embeddedLaunchScript>.."
                                            + ".</embeddedLaunchScript> spring boot JAR file, ";
                            msg +=
                                    "maybe you should upgrade xjar-maven-plugin dependency if it have been supported "
                                            + "in the later versions,";
                            msg +=
                                    "if not, delete <embeddedLaunchScript>...</embeddedLaunchScript> for the "
                                            + "configuration of spring-boot-maven-plugin.";
                            throw new MojoFailureException(msg);
                        }
                    }
                }
                XBoot.encrypt(src, dest, xkey, filter);
            }

            //compress
            myHandle(dest, xkey);

            File root = project.getFile().getParentFile();
            for (int i = 0; deletes != null && i < deletes.length; i++) {
                String delete = deletes[i];
                File dir = root;
                while (delete.startsWith("../")) {
                    dir = dir.getParentFile() != null ? dir.getParentFile() : dir;
                    delete = delete.substring("../".length());
                }

                log.info("Deleting file(s) matching pattern: " + dir.getCanonicalPath().replace('\\', '/') + "/"
                        + delete);

                Enumeration<Resource> resources = Loaders.ant(Loaders.file(dir)).load(delete);
                while (resources.hasMoreElements()) {
                    Resource resource = resources.nextElement();
                    URL url = resource.getUrl();
                    String path = url.getPath();
                    File file = new File(path);
                    log.debug("Deleting file: " + file.getCanonicalPath());
                    if (!file.delete()) {
                        log.warn("Could not delete file: " + file);
                    }
                }
            }

        } catch (MojoExecutionException | MojoFailureException e) {
            throw e;
        } catch (Exception e) {
            throw new MojoExecutionException("could not build xjar", e);
        }
    }

    @SuppressWarnings("ALL")
    private void myHandle(File dest, XKey xkey) throws Exception {
        String goFilePath = targetDir.getAbsolutePath() + "/xjar.go";

        delMaven(dest);
        XGo.make(dest, xkey);

        String sys = System.getProperty("os.name").toLowerCase();
        String archList = arch.stream().reduce((a1, a2) -> a1 + " " + a2).get();
        if (sys.contains(WINDOWS)) {
            String scriptCode =
                    parseScript("xjar/win_build", archList, targetDir.getAbsolutePath(), goFilePath);

            sysBuild(scriptCode, WINDOWS);
        } else if (sys.contains(LINUX)) {
            String scriptCode =
                    parseScript("xjar/linux_build", archList, targetDir.getAbsolutePath(), goFilePath);

            sysBuild(scriptCode, LINUX);
        } else if (sys.contains(MAC)) {
            String scriptCode =
                    parseScript("xjar/linux_build", archList, targetDir.getAbsolutePath(), goFilePath);

            sysBuild(scriptCode, MAC);
        }

        ArrayList<File> wcFileList = new ArrayList<>();
        wcFileList.add(dest);
        for (String prefix : arch) {
            String fp = targetDir.getAbsolutePath() + "/" + prefix + "-xjar";
            File file = new File(fp);
            if (!file.exists()) {
                throw new MojoExecutionException("go build exception, check xjar-plugin");
            }

            file.setReadable(true);
            file.setWritable(true);
            file.setExecutable(true);
            wcFileList.add(file);
        }

        GZIPUtil.compressGZ(wcFileList, targetDir.getAbsolutePath() + "/xjar.tar.gz");
        CompletableFuture.runAsync(() -> wcFileList.forEach(File::delete));
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private void sysBuild(String scriptCode, String operaSystem) throws Exception {
        String script = "";
        String cmd = "";
        if (WINDOWS.equals(operaSystem)) {
            script = targetDir.getAbsolutePath() + "\\goexec.bat";
            cmd = "C:\\Windows\\System32\\cmd.exe /c ";
        } else if (LINUX.equals(operaSystem)) {
            script = targetDir.getAbsolutePath() + "/goexec.sh";
            cmd = "/bin/bash ";
        } else if (MAC.equals(operaSystem)) {
            script = targetDir.getAbsolutePath() + "/goexec.sh";
            cmd = "/bin/bash ";
        }

        FileOutputStream fos = new FileOutputStream(script);
        fos.write(scriptCode.getBytes(StandardCharsets.UTF_8));
        fos.close();

        Process p = Runtime.getRuntime().exec(cmd + script);
        p.waitFor();
        p.destroy();

        new File(script).delete();
    }

    /**
     * 删除jar里面的maven pom.xml
     */
    private void delMaven(File file) throws IOException {
        JarFile jarFile = new JarFile(file);

        List<JarEntry> jarEntryList = jarFile.stream().filter(jarEntry ->
                !jarEntry.getName().contains("META-INF/maven")).collect(Collectors.toList()
        );

        ArrayList<JarEntryWrapper> entryWrappers = new ArrayList<>();
        for (JarEntry jarEntry : jarEntryList) {
            InputStream is = jarFile.getInputStream(jarEntry);
            byte[] data = new byte[is.available()];
            if (is.available() != 0) {
                IOUtils.readFully(is, data);
            }
            entryWrappers.add(new JarEntryWrapper(jarEntry, data));
        }

        String filePath = URLDecoder.decode(file.getAbsolutePath(), "utf-8");

        try (FileOutputStream fos = new FileOutputStream(filePath, false);
             JarOutputStream jos = new JarOutputStream(fos)) {

            fos.write(new byte[0]);
            fos.flush();

            for (JarEntryWrapper wrapper : entryWrappers) {
                jos.putNextEntry(wrapper.getJarEntry());
                jos.write(wrapper.getData());
                jos.flush();
            }
        }

    }

    private String parseScript(String resource, String archList, String targetDir, String goFilePath) {
        try {
            InputStream is = XGo.class.getClassLoader().getResourceAsStream(resource);
            if (is == null) {
                return "";
            }

            byte[] data = new byte[is.available()];
            IOUtils.readFully(is, data);
            is.close();

            String scriptCode = new String(data, StandardCharsets.UTF_8);

            return scriptCode.replace(TPL_ARCH, archList)
                    .replace(TPL_TARGETDIR, targetDir)
                    .replace(TPL_GOFILEPATH, goFilePath);

        } catch (IOException e) {
            e.printStackTrace();
            return "";
        }
    }

    @Data
    @AllArgsConstructor
    private static class JarEntryWrapper {
        private JarEntry jarEntry;
        private byte[] data;
    }


}
